<template>
    <ZxInput
        v-if="!ranged"
        ref="reference"
        v-clickoutside="handleClose"
        class="zx-date-editor"
        :class="'zx-date-editor--' + type"
        :readonly="!editable || readonly || type === 'dates' || type === 'week'"
        :disabled="pickerDisabled"
        :size="pickerSize"
        :name="name"
        v-bind="firstInputId"
        :placeholder="placeholder"
        :value="displayValue"
        :validate-event="false"
        @focus="handleFocus"
        @keydown.native="handleKeydown"
        @input="value => userInput = value"
        @change="handleChange"
        @mouseenter.native="handleMouseEnter"
        @mouseleave.native="showClose = false"
    >
        <i
            slot="prefix"
            style="fontSize:16px"
            class="zx-input__icon"
            :class="triggerClass"
            @click="handleFocus"
        />
        <i
            v-if="haveTrigger"
            slot="suffix"
            style="fontSize:16px"
            class="zx-input__icon"
            :class="[showClose ? '' + clearIcon : '']"
            @click="handleClickIcon"
        />
    </ZxInput>
    <div
        v-else
        ref="reference"
        v-clickoutside="handleClose"
        class="zx-date-editor zx-range-editor zx-input__inner"
        :class="[
            'zx-date-editor--' + type,
            pickerSize ? `zx-range-editor--${ pickerSize }` : '',
            pickerDisabled ? 'is-disabled' : '',
            pickerVisible ? 'is-active' : ''
        ]"
        @click="handleRangeClick"
        @mouseenter="handleMouseEnter"
        @mouseleave="showClose = false"
        @keydown="handleKeydown"
    >
        <i :class="['zx-input__icon', 'zx-range__icon', triggerClass]" style="fontSize:16px" />
        <input
            autocomplete="off"
            :placeholder="startPlaceholder"
            :value="displayValue && displayValue[0]"
            :disabled="pickerDisabled"
            v-bind="firstInputId"
            :readonly="!editable || readonly"
            :name="name && name[0]"
            class="zx-range-input"
            @input="handleStartInput"
            @change="handleStartChange"
            @focus="handleFocus"
        >
        <slot name="range-separator">
            <span class="zx-range-separator">{{ rangeSeparator }}</span>
        </slot>
        <input
            autocomplete="off"
            :placeholder="endPlaceholder"
            :value="displayValue && displayValue[1]"
            :disabled="pickerDisabled"
            v-bind="secondInputId"
            :readonly="!editable || readonly"
            :name="name && name[1]"
            class="zx-range-input"
            @input="handleEndInput"
            @change="handleEndChange"
            @focus="handleFocus"
        >
        <i
            v-if="haveTrigger"
            :class="[showClose ? '' + clearIcon : '']"
            class="zx-input__icon zx-range__close-icon"
            @click="handleClickIcon"
        />
    </div>
</template>

<script>
import Vue from 'vue';
import Clickoutside from '@src/utils/clickoutside';
import { formatDate, parseDate, isDateObject, getWeekNumber } from '@src/utils/date-util';
import Popper from '@src/utils/vue-popper';
import Emitter from '@src/mixins/emitter';
import ZxInput from '../../../components/input';
import merge from '@src/utils/merge';

const NewPopper = {
    props: {
        appendToBody: Popper.props.appendToBody,
        offset: Popper.props.offset,
        boundariesPadding: Popper.props.boundariesPadding,
        arrowOffset: Popper.props.arrowOffset,
    },
    methods: Popper.methods,
    data() {
        return merge({ visibleArrow: true }, Popper.data);
    },
    beforeDestroy: Popper.beforeDestroy,
};

const DEFAULT_FORMATS = {
    date: 'yyyy-MM-dd',
    month: 'yyyy-MM',
    datetime: 'yyyy-MM-dd HH:mm:ss',
    time: 'HH:mm:ss',
    week: 'yyyywWW',
    timerange: 'HH:mm:ss',
    daterange: 'yyyy-MM-dd',
    monthrange: 'yyyy-MM',
    datetimerange: 'yyyy-MM-dd HH:mm:ss',
    year: 'yyyy',
};
const HAVE_TRIGGER_TYPES = [
    'date',
    'datetime',
    'time',
    'time-select',
    'week',
    'month',
    'year',
    'daterange',
    'monthrange',
    'timerange',
    'datetimerange',
    'dates',
];
const DATE_FORMATTER = function (value, format) {
    if (format === 'timestamp') return value.getTime();
    return formatDate(value, format);
};
const DATE_PARSER = function (text, format) {
    if (format === 'timestamp') return new Date(Number(text));
    return parseDate(text, format);
};
const RANGE_FORMATTER = function (value, format) {
    if (Array.isArray(value) && value.length === 2) {
        const start = value[0];
        const end = value[1];

        if (start && end) {
            return [DATE_FORMATTER(start, format), DATE_FORMATTER(end, format)];
        }
    }
    return '';
};
const RANGE_PARSER = function (array, format, separator) {
    if (!Array.isArray(array)) {
        array = array.split(separator);
    }
    if (array.length === 2) {
        const range1 = array[0];
        const range2 = array[1];

        return [DATE_PARSER(range1, format), DATE_PARSER(range2, format)];
    }
    return [];
};
const TYPE_VALUE_RESOLVER_MAP = {
    default: {
        formatter(value) {
            if (!value) return '';
            return '' + value;
        },
        parser(text) {
            if (text === undefined || text === '') return null;
            return text;
        },
    },
    week: {
        formatter(value, format) {
            const week = getWeekNumber(value);
            const month = value.getMonth();
            const trueDate = new Date(value);
            if (week === 1 && month === 11) {
                trueDate.setHours(0, 0, 0, 0);
                trueDate.setDate(trueDate.getDate() + 3 - (trueDate.getDay() + 6) % 7);
            }
            let date = formatDate(trueDate, format);

            date = /WW/.test(date)
                ? date.replace(/WW/, week < 10 ? '0' + week : week)
                : date.replace(/W/, week);
            return date;
        },
        parser(text, format) {
            // parse as if a normal date
            return TYPE_VALUE_RESOLVER_MAP.date.parser(text, format);
        },
    },
    date: {
        formatter: DATE_FORMATTER,
        parser: DATE_PARSER,
    },
    datetime: {
        formatter: DATE_FORMATTER,
        parser: DATE_PARSER,
    },
    daterange: {
        formatter: RANGE_FORMATTER,
        parser: RANGE_PARSER,
    },
    monthrange: {
        formatter: RANGE_FORMATTER,
        parser: RANGE_PARSER,
    },
    datetimerange: {
        formatter: RANGE_FORMATTER,
        parser: RANGE_PARSER,
    },
    timerange: {
        formatter: RANGE_FORMATTER,
        parser: RANGE_PARSER,
    },
    time: {
        formatter: DATE_FORMATTER,
        parser: DATE_PARSER,
    },
    month: {
        formatter: DATE_FORMATTER,
        parser: DATE_PARSER,
    },
    year: {
        formatter: DATE_FORMATTER,
        parser: DATE_PARSER,
    },
    number: {
        formatter(value) {
            if (!value) return '';
            return '' + value;
        },
        parser(text) {
            const result = Number(text);

            if (!isNaN(text)) {
                return result;
            } else {
                return null;
            }
        },
    },
    dates: {
        formatter(value, format) {
            return value.map(date => DATE_FORMATTER(date, format));
        },
        parser(value, format) {
            return (typeof value === 'string' ? value.split(', ') : value)
                .map(date => date instanceof Date ? date : DATE_PARSER(date, format));
        },
    },
};
const PLACEMENT_MAP = {
    left: 'bottom-start',
    center: 'bottom',
    right: 'bottom-end',
};

const parseAsFormatAndType = (value, customFormat, type, rangeSeparator = '-') => {
    if (!value) return null;
    const parser = (
        TYPE_VALUE_RESOLVER_MAP[type] ||
        TYPE_VALUE_RESOLVER_MAP['default']
    ).parser;
    const format = customFormat || DEFAULT_FORMATS[type];
    return parser(value, format, rangeSeparator);
};

const formatAsFormatAndType = (value, customFormat, type) => {
    if (!value) return null;
    const formatter = (
        TYPE_VALUE_RESOLVER_MAP[type] ||
        TYPE_VALUE_RESOLVER_MAP['default']
    ).formatter;
    const format = customFormat || DEFAULT_FORMATS[type];
    return formatter(value, format);
};

/*
 * Considers:
 *   1. Date object
 *   2. date string
 *   3. array of 1 or 2
 */
const valueEquals = function (a, b) {
    // considers Date object and string
    const dateEquals = function (a, b) {
        const aIsDate = a instanceof Date;
        const bIsDate = b instanceof Date;
        if (aIsDate && bIsDate) {
            return a.getTime() === b.getTime();
        }
        if (!aIsDate && !bIsDate) {
            return a === b;
        }
        return false;
    };

    const aIsArray = a instanceof Array;
    const bIsArray = b instanceof Array;
    if (aIsArray && bIsArray) {
        if (a.length !== b.length) {
            return false;
        }
        return a.every((item, index) => dateEquals(item, b[index]));
    }
    if (!aIsArray && !bIsArray) {
        return dateEquals(a, b);
    }
    return false;
};

const isString = function (val) {
    return typeof val === 'string' || val instanceof String;
};

const validator = function (val) {
    // either: String, Array of String, null / undefined
    return (
        val === null ||
        val === undefined ||
        isString(val) ||
        (Array.isArray(val) && val.length === 2 && val.every(isString))
    );
};

export default {

    components: { ZxInput },

    directives: { Clickoutside },
    mixins: [Emitter, NewPopper],

    inject: {
        zxForm: {
            default: '',
        },
        zxFormItem: {
            default: '',
        },
    },

    props: {
        size: String,
        format: String,
        valueFormat: String,
        readonly: Boolean,
        placeholder: String,
        startPlaceholder: String,
        endPlaceholder: String,
        prefixIcon: String,
        clearIcon: {
            type: String,
            default: 'zx-icon-close-circle-fill',
        },
        name: {
            default: '',
            validator,
        },
        disabled: Boolean,
        clearable: {
            type: Boolean,
            default: true,
        },
        id: {
            default: '',
            validator,
        },
        popperClass: String,
        editable: {
            type: Boolean,
            default: true,
        },
        align: {
            type: String,
            default: 'left',
        },
        value: {},
        defaultValue: {},
        defaultTime: {},
        rangeSeparator: {
            default: '-',
        },
        pickerOptions: {},
        unlinkPanels: Boolean,
        validateEvent: {
            type: Boolean,
            default: true,
        },
    },

    data() {
        return {
            pickerVisible: false,
            showClose: false,
            userInput: null,
            valueOnOpen: null, // value when picker opens, used to determine whether to emit change
            unwatchPickerOptions: null,
        };
    },

    computed: {
        ranged() {
            return this.type.indexOf('range') > -1;
        },

        reference() {
            const reference = this.$refs.reference;
            return reference.$el || reference;
        },

        refInput() {
            if (this.reference) {
                return [].slice.call(this.reference.querySelectorAll('input'));
            }
            return [];
        },

        valueIsEmpty() {
            const val = this.value;
            if (Array.isArray(val)) {
                for (let i = 0, len = val.length; i < len; i++) {
                    if (val[i]) {
                        return false;
                    }
                }
            } else {
                if (val) {
                    return false;
                }
            }
            return true;
        },

        triggerClass() {
            return this.prefixIcon || (this.type.indexOf('time') !== -1 ? 'zx-icon-time' : 'zx-icon-date');
        },

        selectionMode() {
            if (this.type === 'week') {
                return 'week';
            } else if (this.type === 'month') {
                return 'month';
            } else if (this.type === 'year') {
                return 'year';
            } else if (this.type === 'dates') {
                return 'dates';
            }

            return 'day';
        },

        haveTrigger() {
            if (typeof this.showTrigger !== 'undefined') {
                return this.showTrigger;
            }
            return HAVE_TRIGGER_TYPES.indexOf(this.type) !== -1;
        },

        displayValue() {
            const formattedValue = formatAsFormatAndType(this.parsedValue, this.format, this.type, this.rangeSeparator);
            if (Array.isArray(this.userInput)) {
                return [
                    this.userInput[0] || (formattedValue && formattedValue[0]) || '',
                    this.userInput[1] || (formattedValue && formattedValue[1]) || '',
                ];
            } else if (this.userInput !== null) {
                return this.userInput;
            } else if (formattedValue) {
                return this.type === 'dates'
                    ? formattedValue.join(', ')
                    : formattedValue;
            } else {
                return '';
            }
        },

        parsedValue() {
            if (!this.value) return this.value; // component value is not set
            if (this.type === 'time-select') return this.value; // time-select does not require parsing, this might change in next major version

            const valueIsDateObject = isDateObject(this.value) || (Array.isArray(this.value) && this.value.every(isDateObject));
            if (valueIsDateObject) {
                return this.value;
            }

            if (this.valueFormat) {
                return parseAsFormatAndType(this.value, this.valueFormat, this.type, this.rangeSeparator) || this.value;
            }

            // NOTE: deal with common but incorrect usage, should remove in next major version
            // user might provide string / timestamp without value-format, coerce them into date (or array of date)
            return Array.isArray(this.value) ? this.value.map(val => new Date(val)) : new Date(this.value);
        },

        _zxFormItemSize() {
            return (this.zxFormItem || {}).zxFormItemSize;
        },

        pickerSize() {
            return this.size || this._zxFormItemSize || (this.$ELEMENT || {}).size;
        },

        pickerDisabled() {
            return this.disabled || (this.zxForm || {}).disabled;
        },

        firstInputId() {
            const obj = {};
            let id;
            if (this.ranged) {
                id = this.id && this.id[0];
            } else {
                id = this.id;
            }
            if (id) obj.id = id;
            return obj;
        },

        secondInputId() {
            const obj = {};
            let id;
            if (this.ranged) {
                id = this.id && this.id[1];
            }
            if (id) obj.id = id;
            return obj;
        },
    },

    watch: {
        pickerVisible(val) {
            if (this.readonly || this.pickerDisabled) return;
            if (val) {
                this.showPicker();
                this.valueOnOpen = Array.isArray(this.value) ? [...this.value] : this.value;
            } else {
                this.hidePicker();
                this.emitChange(this.value);
                this.userInput = null;
                if (this.validateEvent) {
                    this.dispatch('ZxFormItem', 'el.form.blur');
                }
                this.$emit('blur', this);
                this.blur();
            }
        },
        parsedValue: {
            immediate: true,
            handler(val) {
                if (this.picker) {
                    this.picker.value = val;
                }
            },
        },
        defaultValue(val) {
            // NOTE: should eventually move to jsx style picker + panel ?
            if (this.picker) {
                this.picker.defaultValue = val;
            }
        },
        value(val, oldVal) {
            if (!valueEquals(val, oldVal) && !this.pickerVisible && this.validateEvent) {
                this.dispatch('ZxFormItem', 'el.form.change', val);
            }
        },
    },

    created() {
        // vue-popper
        this.popperOptions = {
            boundariesPadding: 0,
            gpuAcceleration: false,
        };
        this.placement = PLACEMENT_MAP[this.align] || PLACEMENT_MAP.left;

        this.$on('fieldReset', this.handleFieldReset);
    },

    methods: {
        focus() {
            if (!this.ranged) {
                this.$refs.reference.focus();
            } else {
                this.handleFocus();
            }
        },

        blur() {
            this.refInput.forEach(input => input.blur());
        },

        // {parse, formatTo} Value deals maps component value with internal Date
        parseValue(value) {
            const isParsed = isDateObject(value) || (Array.isArray(value) && value.every(isDateObject));
            if (this.valueFormat && !isParsed) {
                return parseAsFormatAndType(value, this.valueFormat, this.type, this.rangeSeparator) || value;
            } else {
                return value;
            }
        },

        formatToValue(date) {
            const isFormattable = isDateObject(date) || (Array.isArray(date) && date.every(isDateObject));
            if (this.valueFormat && isFormattable) {
                return formatAsFormatAndType(date, this.valueFormat, this.type, this.rangeSeparator);
            } else {
                return date;
            }
        },

        // {parse, formatTo} String deals with user input
        parseString(value) {
            const type = Array.isArray(value) ? this.type : this.type.replace('range', '');
            return parseAsFormatAndType(value, this.format, type);
        },

        formatToString(value) {
            const type = Array.isArray(value) ? this.type : this.type.replace('range', '');
            return formatAsFormatAndType(value, this.format, type);
        },

        handleMouseEnter() {
            if (this.readonly || this.pickerDisabled) return;
            if (!this.valueIsEmpty && this.clearable) {
                this.showClose = true;
            }
        },

        handleChange() {
            if (this.userInput) {
                const value = this.parseString(this.displayValue);
                if (value) {
                    this.picker.value = value;
                    if (this.isValidValue(value)) {
                        this.emitInput(value);
                        this.userInput = null;
                    }
                }
            }
            if (this.userInput === '') {
                this.emitInput(null);
                this.emitChange(null);
                this.userInput = null;
            }
        },

        handleStartInput(event) {
            if (this.userInput) {
                this.userInput = [event.target.value, this.userInput[1]];
            } else {
                this.userInput = [event.target.value, null];
            }
        },

        handleEndInput(event) {
            if (this.userInput) {
                this.userInput = [this.userInput[0], event.target.value];
            } else {
                this.userInput = [null, event.target.value];
            }
        },

        handleStartChange(event) {
            const value = this.parseString(this.userInput && this.userInput[0]);
            if (value) {
                this.userInput = [this.formatToString(value), this.displayValue[1]];
                const newValue = [value, this.picker.value && this.picker.value[1]];
                this.picker.value = newValue;
                if (this.isValidValue(newValue)) {
                    this.emitInput(newValue);
                    this.userInput = null;
                }
            }
        },

        handleEndChange(event) {
            const value = this.parseString(this.userInput && this.userInput[1]);
            if (value) {
                this.userInput = [this.displayValue[0], this.formatToString(value)];
                const newValue = [this.picker.value && this.picker.value[0], value];
                this.picker.value = newValue;
                if (this.isValidValue(newValue)) {
                    this.emitInput(newValue);
                    this.userInput = null;
                }
            }
        },

        handleClickIcon(event) {
            if (this.readonly || this.pickerDisabled) return;
            if (this.showClose) {
                this.valueOnOpen = this.value;
                event.stopPropagation();
                this.emitInput(null);
                this.emitChange(null);
                this.showClose = false;
                if (this.picker && typeof this.picker.handleClear === 'function') {
                    this.picker.handleClear();
                }
            } else {
                this.pickerVisible = !this.pickerVisible;
            }
        },

        handleClose() {
            if (!this.pickerVisible) return;
            this.pickerVisible = false;

            if (this.type === 'dates') {
                // restore to former value
                const oldValue = parseAsFormatAndType(this.valueOnOpen, this.valueFormat, this.type, this.rangeSeparator) || this.valueOnOpen;
                this.emitInput(oldValue);
            }
        },

        handleFieldReset(initialValue) {
            this.userInput = initialValue === '' ? null : initialValue;
        },

        handleFocus() {
            const type = this.type;

            if (HAVE_TRIGGER_TYPES.indexOf(type) !== -1 && !this.pickerVisible) {
                this.pickerVisible = true;
            }
            this.$emit('focus', this);
        },

        handleKeydown(event) {
            const keyCode = event.keyCode;

            // ESC
            if (keyCode === 27) {
                this.pickerVisible = false;
                event.stopPropagation();
                return;
            }

            // Tab
            if (keyCode === 9) {
                if (!this.ranged) {
                    this.handleChange();
                    this.pickerVisible = this.picker.visible = false;
                    this.blur();
                    event.stopPropagation();
                } else {
                    // user may change focus between two input
                    setTimeout(() => {
                        if (this.refInput.indexOf(document.activeElement) === -1) {
                            this.pickerVisible = false;
                            this.blur();
                            event.stopPropagation();
                        }
                    }, 0);
                }
                return;
            }

            // Enter
            if (keyCode === 13) {
                if (this.userInput === '' || this.isValidValue(this.parseString(this.displayValue))) {
                    this.handleChange();
                    this.pickerVisible = this.picker.visible = false;
                    this.blur();
                }
                event.stopPropagation();
                return;
            }

            // if user is typing, do not let picker handle key input
            if (this.userInput) {
                event.stopPropagation();
                return;
            }

            // delegate other keys to panel
            if (this.picker && this.picker.handleKeydown) {
                this.picker.handleKeydown(event);
            }
        },

        handleRangeClick() {
            const type = this.type;

            if (HAVE_TRIGGER_TYPES.indexOf(type) !== -1 && !this.pickerVisible) {
                this.pickerVisible = true;
            }
            this.$emit('focus', this);
        },

        hidePicker() {
            if (this.picker) {
                this.picker.resetView && this.picker.resetView();
                this.pickerVisible = this.picker.visible = false;
                this.destroyPopper();
            }
        },

        showPicker() {
            if (this.$isServer) return;
            if (!this.picker) {
                this.mountPicker();
            }
            this.pickerVisible = this.picker.visible = true;

            this.updatePopper();

            this.picker.value = this.parsedValue;
            this.picker.resetView && this.picker.resetView();

            this.$nextTick(() => {
                this.picker.adjustSpinners && this.picker.adjustSpinners();
            });
        },

        mountPicker() {
            this.picker = new Vue(this.panel).$mount();
            this.picker.defaultValue = this.defaultValue;
            this.picker.defaultTime = this.defaultTime;
            this.picker.popperClass = this.popperClass;
            this.popperElm = this.picker.$el;
            this.picker.width = this.reference.getBoundingClientRect().width;
            this.picker.showTime = this.type === 'datetime' || this.type === 'datetimerange';
            this.picker.selectionMode = this.selectionMode;
            this.picker.unlinkPanels = this.unlinkPanels;
            this.picker.arrowControl = this.arrowControl || this.timeArrowControl || false;
            this.$watch('format', (format) => {
                this.picker.format = format;
            });

            const updateOptions = () => {
                const options = this.pickerOptions;

                if (options && options.selectableRange) {
                    let ranges = options.selectableRange;
                    const parser = TYPE_VALUE_RESOLVER_MAP.datetimerange.parser;
                    const format = DEFAULT_FORMATS.timerange;

                    ranges = Array.isArray(ranges) ? ranges : [ranges];
                    this.picker.selectableRange = ranges.map(range => parser(range, format, this.rangeSeparator));
                }

                for (const option in options) {
                    if (options.hasOwnProperty(option) &&
                        // 忽略 time-picker 的该配置项
                        option !== 'selectableRange') {
                        this.picker[option] = options[option];
                    }
                }

                // main format must prevail over undocumented pickerOptions.format
                if (this.format) {
                    this.picker.format = this.format;
                }
            };
            updateOptions();
            this.unwatchPickerOptions = this.$watch('pickerOptions', () => updateOptions(), { deep: true });
            this.$el.appendChild(this.picker.$el);
            this.picker.resetView && this.picker.resetView();

            this.picker.$on('dodestroy', this.doDestroy);
            this.picker.$on('pick', (date = '', visible = false) => {
                this.userInput = null;
                this.pickerVisible = this.picker.visible = visible;
                this.emitInput(date);
                this.picker.resetView && this.picker.resetView();
            });

            this.picker.$on('select-range', (start, end, pos) => {
                if (this.refInput.length === 0) return;
                if (!pos || pos === 'min') {
                    this.refInput[0].setSelectionRange(start, end);
                    this.refInput[0].focus();
                } else if (pos === 'max') {
                    this.refInput[1].setSelectionRange(start, end);
                    this.refInput[1].focus();
                }
            });
        },

        unmountPicker() {
            if (this.picker) {
                this.picker.$destroy();
                this.picker.$off();
                if (typeof this.unwatchPickerOptions === 'function') {
                    this.unwatchPickerOptions();
                }
                this.picker.$el.parentNode.removeChild(this.picker.$el);
            }
        },

        emitChange(val) {
            // determine user real change only
            if (!valueEquals(val, this.valueOnOpen)) {
                this.$emit('change', val);
                this.valueOnOpen = val;
                if (this.validateEvent) {
                    this.dispatch('ZxFormItem', 'el.form.change', val);
                }
            }
        },

        emitInput(val) {
            const formatted = this.formatToValue(val);
            if (!valueEquals(this.value, formatted)) {
                this.$emit('input', formatted);
            }
        },

        isValidValue(value) {
            if (!this.picker) {
                this.mountPicker();
            }
            if (this.picker.isValidValue) {
                return value && this.picker.isValidValue(value);
            } else {
                return true;
            }
        },
    },
};
</script>
